﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using Sharebert.OverlayLibrary.API;
using Sharebert.LinkLayerLibrary.API;
using Sharebert.LinkLayerLibrary;
using System.Threading;
using System.Timers;
using System.Net.Sockets;
using System.Net;
using Sharebert.LoggingLibrary.API;

namespace Sharebert.OverlayLibrary
{
    /// <summary>
    /// BootstrapClient is used to communicate with the bootstrap server. The bootstrap server provides a list 
    /// of currently active peers. 
    /// </summary>
    public class BootstrapClient : IBootstrapClient
    {

        private const int DEFAULT_TIMEOUT = 10;


        /// <summary>
        /// Initializes a new instance of the BootstrapClient class. 
        /// </summary>
        /// <param name="serverAddress">Address of the bootstrap server</param>
        /// <param name="serverPort">Port of the bootstrap server</param>
        /// <param name="listenIP">IP address used to listen on new peer connections</param>
        /// <param name="listenPort">The port used to listen for new clients</param>
        public BootstrapClient(string serverAddress, int serverPort, string listenIP, int listenPort, ILogger logger)
        {
            this.ServerAddress = serverAddress;
            this.ServerPort = serverPort;
            this.ListenIP = listenIP;
            this.ListenPort = listenPort; 
            this.Timeout = DEFAULT_TIMEOUT;
            this.Logger = logger;

            this.connection = new Connection();
            this.connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            this.connection.ConnectionClosed += new EventHandler<EventArgs>(HandleConnectionClosed);
            this.connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleConnectionTimedOut);
        }

        /// <summary>
        /// Initializes a new instance of the BootstrapClient class. A random port greater than 1023 is used to listen for new peer connections.
        /// </summary>
        /// <param name="serverAddress">Address of the bootstrap server</param>
        /// <param name="serverPort">Port of the bootstrap server</param>
        /// <param name="listenIP">IP address used to listen on new peer connections</param>
        public BootstrapClient(string serverAddress, int serverPort, string listenIP, ILogger logger)
            : this(serverAddress, serverPort, listenIP, GenerateRandomListenPort(listenIP), logger)
        {
        }

        /// <summary>
        /// Requests a list of active peers from the bootstrap server.
        /// </summary>
        /// <param name="n">The number of peers, that the server should give</param>
        /// <exception cref="NetworkException">Could not connect or sending message failed</exception>
        public void RequestPeers(int n)
        {
            PeerListRequest plr = new PeerListRequest(this.ListenPort, n);
            byte[] payload = Serialisation.Serialize(plr, typeof(PeerListRequest));
            IHeader header = new Header(MessageTypes.PEERLIST_REQUEST, payload.Length);
            IMessage message = new Message(header, payload);

            try
            {
                this.connection.WriteTimeout = this.Timeout;
                this.connection.ReadTimeout = this.Timeout;
                this.connection.Open(this.ServerAddress, this.ServerPort);
                this.connection.SendMessage(message);
            }
            catch (Exception ex)
            {
                this.connection.Close();
                throw ex;
            }
        }

        /// <summary>
        /// Sends a ping to the bootstrap server to signal that the peer is alive.
        /// </summary>
        /// <exception cref="NetworkException">Could not connect or sending message failed</exception>
        public void PingBootstrapServer()
        {
            Ping ping = new Ping(this.ListenPort);
            byte[] payload = Serialisation.Serialize(ping, typeof(Ping));
            IHeader header = new Header(MessageTypes.PING, payload.Length);
            IMessage message = new Message(header, payload);

            try
            {
                this.connection.WriteTimeout = this.Timeout;
                this.connection.ReadTimeout = this.Timeout;
                this.connection.Open(this.ServerAddress, this.ServerPort);
                this.connection.SendMessage(message);
                this.Logger.Debug("Ping bootstrap server");
            }
            finally
            {
                this.connection.Close();
            }
        }

        /// <summary>
        /// Closes the connection to the bootstrap server.
        /// </summary>
        /// <exception cref="NetworkException">Could not connect to server or send message</exception>
        public void Offline()
        {
            Offline offline = new Offline(this.ListenPort);
            byte[] payload = Serialisation.Serialize(offline, typeof(Offline));
            IHeader header = new Header(MessageTypes.OFFLINE, payload.Length);
            IMessage message = new Message(header, payload);

            try
            {
                this.connection.Open(this.ServerAddress, this.ServerPort);
                this.connection.SendMessage(message);
            }
            finally
            {
                // Always close the connection
                this.connection.Close();
            }
        }

        private static int GenerateRandomListenPort(string listenIP)
        {
            IPAddress ip = IPAddress.Parse(listenIP);
            Random rnd = new Random();
            while(true)
            {
                try
                {
                    int newPort = rnd.Next(1024, IPEndPoint.MaxPort);
                    IPEndPoint ep = new IPEndPoint(ip, newPort);
                    Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    socket.Bind(ep);

                    // Port is not used
                    socket.Close();
                    return newPort;
                }
                catch(SocketException ex)
                {
                    if (!ex.SocketErrorCode.Equals(SocketError.AddressAlreadyInUse))
                    {
                        throw new NetworkException("Could not select random listen port!", ex);
                    }
                }
            }
        }

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {
            try
            {
                if (e.Message.Header.Command.Equals(MessageTypes.PEERLIST_RESPONSE))
                {
                    if (this.PeerListReceived != null)
                    {

                        this.PeerListReceived.Invoke(this, new PeerListReceivedEventArgs((PeerList)Serialisation.Deserialize(e.Message.Payload, typeof(PeerList))));
                    }
                }
            }
            finally
            {
                this.connection.Close();
            }
        }

        private void HandleConnectionClosed(object sender, EventArgs e)
        {
            if (this.ConnectionLost != null)
            {
                this.ConnectionLost.Invoke(this, new EventArgs());
            }
            this.connection.Close();
        }

        private void HandleConnectionTimedOut(object sender, EventArgs e)
        {
            if (this.ConnectionLost != null)
            {
                this.ConnectionLost.Invoke(this, new EventArgs());
            }
            this.connection.Close();
        }


        #region Properties and private member

        /// <summary>
        /// The hostname or IP of the bootstrap server.
        /// </summary>
        public string ServerAddress { get; private set; }

        /// <summary>
        /// The port of the bootstrap server.
        /// </summary>
        public int ServerPort { get; private set; }

        /// <summary>
        /// The IP to listen for new peer connections.
        /// </summary>
        public string ListenIP { get; private set; }

        /// <summary>
        /// The port to listen for new peer connections.
        /// </summary>
        public int ListenPort { get; private set; }

        /// <summary>
        /// Sending and receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int Timeout { get; set; }

        /// <summary>
        /// The logger used to log messages.
        /// </summary>
        public ILogger Logger { get; private set; }

        private IConnection connection;

        #endregion


        #region Events

        /// <summary>
        /// The server did not respond after Timeout seconds.
        /// </summary>
        public event EventHandler<EventArgs> ConnectionLost;

        /// <summary>
        /// Received a peer list from the server.
        /// </summary>
        public event EventHandler<PeerListReceivedEventArgs> PeerListReceived;

        #endregion

    }
}
